﻿#region License
// (c) Intergen.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
#endregion

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using TextGlow.Control.Utilities;

namespace TextGlow.Control.Controls
{
  public class WrapPanel : UserControlExtended
  {
    public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(WrapPanel), null);
    public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(WrapPanel), null);
    public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(WrapPanel), null);
    public static readonly DependencyProperty RowHorizontalAlignmentProperty = DependencyProperty.Register("RowHorizontalAlignment", typeof(HorizontalAlignment), typeof(WrapPanel), null);
    public static readonly DependencyProperty ColumnVerticalAlignmentProperty = DependencyProperty.Register("ColumnVerticalAlignment", typeof(VerticalAlignment), typeof(WrapPanel), null);

    private int _lines;
    private Canvas _canvas;

    public UIElementCollection Children
    {
      get { return _canvas.Children; }
    }

    public double ItemHeight
    {
      get { return (double)GetValue(ItemHeightProperty); }
      set { SetValue(ItemHeightProperty, value); }
    }

    public double ItemWidth
    {
      get { return (double)GetValue(ItemWidthProperty); }
      set { base.SetValue(ItemWidthProperty, value); }
    }

    public Orientation Orientation
    {
      get { return (Orientation)GetValue(OrientationProperty); }
      set { SetValue(OrientationProperty, value); }
    }

    public HorizontalAlignment RowHorizontalAlignment
    {
      get { return (HorizontalAlignment)GetValue(RowHorizontalAlignmentProperty); }
      set { SetValue(RowHorizontalAlignmentProperty, value); }
    }

    public VerticalAlignment ColumnVerticalAlignment
    {
      get { return (VerticalAlignment)GetValue(ColumnVerticalAlignmentProperty); }
      set { SetValue(ColumnVerticalAlignmentProperty, value); }
    }

    public int Lines
    {
      get { return _lines; }
      protected set { _lines = value; }
    }

    public WrapPanel()
    {
      _canvas = new Canvas();
      Content = _canvas;

      RowHorizontalAlignment = HorizontalAlignment.Left;
      ColumnVerticalAlignment = VerticalAlignment.Top;
    }

    protected enum Offset
    {
      Near,
      Center,
      Far,
      Strech
    }

    protected Offset GetLineOffset()
    {
      return (Offset)((IsHorizontal) ? (int)RowHorizontalAlignment : (int)ColumnVerticalAlignment);
    }

    protected Offset GetElementOffset(UIElement element)
    {
      FrameworkElement f = element as FrameworkElement;
      if (f == null)
        return Offset.Near;

      return (Offset)((IsHorizontal) ? (int)f.VerticalAlignment : (int)f.HorizontalAlignment);
    }

    protected bool IsHorizontal
    {
      get { return (Orientation == Orientation.Horizontal); }
    }

    protected virtual bool ArrangeLine(double v, double lineV, double availableV, int start, int end, bool useItemU, double itemU, double lineRemainder)
    {
      double totalLineOffset = 0.0;
      Offset offsetType = GetLineOffset();

      if (offsetType == Offset.Center)
        totalLineOffset += lineRemainder / 2;
      else if (offsetType == Offset.Far)
        totalLineOffset += lineRemainder;

      for (int i = start; i < end; i++)
      {
        UIElement element = Children[i];
        if (element != null)
        {
          UVSize size = new UVSize(Orientation, CalculateElementSize(element));
          Offset elementOffset = GetElementOffset(element);
          double itemOffset = useItemU ? itemU : size.U;
          double itemV = v + CalculateItemV(size, elementOffset, availableV);
          Rect elementRect = new Rect(
            IsHorizontal ? totalLineOffset : itemV,
            IsHorizontal ? itemV : totalLineOffset,
            IsHorizontal ? itemOffset : size.V,
            IsHorizontal ? size.V : itemOffset
            );
          ArrangeElement(element, elementRect);
          totalLineOffset += itemOffset;
          if (offsetType == Offset.Strech)
            totalLineOffset += lineRemainder / (end - start);
        }
      }

      Lines = Lines + 1;
      return true;
    }

    private double CalculateItemV(UVSize size, Offset elementOffset, double lineV)
    {
      switch (elementOffset)
      {
        case Offset.Near:
          return 0;
        case Offset.Center:
          return (lineV - size.V) / 2;
        case Offset.Far:
          return lineV - size.V;
        case Offset.Strech:
          return 0;
        default:
          throw new ArgumentOutOfRangeException("elementOffset", string.Format("Unexpected element offset type: {0}", elementOffset));
      }
    }

    private void ArrangeElement(UIElement element, Rect finalRect)
    {
      element.SetLocation(finalRect.GetTopLeft());
      UserControlExtended customArrageElement = element as UserControlExtended;
      if (customArrageElement != null)
        customArrageElement.CustomArrange(finalRect);
      else
        element.Arrange(finalRect);
    }

    private void MeasureElement(UIElement element, Size availableSize)
    {
      UserControlExtended customArrageElement = element as UserControlExtended;
      if (customArrageElement != null)
        customArrageElement.CustomMeasure(availableSize);
      else
        element.Measure(availableSize);
    }

    protected virtual Size CalculateElementSize(UIElement element)
    {
      double width;
      double height;

      UserControlExtended customArrageElement = element as UserControlExtended;
      if (customArrageElement != null)
      {
        width = HasItemWidth ? ItemWidth : customArrageElement.CustomDesiredSize.Width;
        height = HasItemHeight ? ItemHeight : customArrageElement.CustomDesiredSize.Height;
      }
      else
      {
        width = HasItemWidth ? ItemWidth : element.DesiredSize.Width;
        height = HasItemHeight ? ItemHeight : element.DesiredSize.Height;
      }

      return new Size(width, height);
    }

    protected virtual bool MeasureLine(int start, int end, double lineRemainder, ref UVSize lineSize)
    {
      Lines = Lines + 1;
      return true;
    }

    protected override Size CustomArrangeOverride(Size finalSize)
    {
      Lines = 0;

      int start = 0;
      double itemWidth = ItemWidth;
      double itemHeight = ItemHeight;
      double v = 0.0;
      double itemU = (IsHorizontal) ? itemWidth : itemHeight;
      UVSize lineSize = new UVSize(Orientation);
      UVSize constraintSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
      bool useItemU = (IsHorizontal)
        ? (!double.IsNaN(itemWidth) && itemWidth != 0)
        : (!double.IsNaN(itemHeight) && itemHeight != 0);
      int end = 0;

      while (end < Children.Count)
      {
        UIElement element = Children[end];
        if (element != null)
        {
          UVSize currentElementSize = new UVSize(Orientation, CalculateElementSize(element));
          if ((lineSize.U + currentElementSize.U).GreaterThan(constraintSize.U))
          {
            if (!ArrangeLine(v, lineSize.V, constraintSize.V, start, end, useItemU, itemU, constraintSize.U - lineSize.U))
              return finalSize;
            v += lineSize.V;
            lineSize = currentElementSize;
            if (currentElementSize.U.GreaterThan(constraintSize.U))
            {
              if (!ArrangeLine(v, currentElementSize.V, constraintSize.V, end, ++end, useItemU, itemU, 0))
                return finalSize;
              v += currentElementSize.V;
              lineSize = new UVSize(Orientation);
            }
            start = end;
          }
          else
          {
            lineSize.U += currentElementSize.U;
            lineSize.V = Math.Max(currentElementSize.V, lineSize.V);
          }
        }
        end++;
      }

      if (start < Children.Count)
        ArrangeLine(v, lineSize.V, constraintSize.V, start, Children.Count, useItemU, itemU, constraintSize.U - lineSize.U);

      return finalSize;
    }

    private static bool IsWidthHeightValid(object value)
    {
      double num = (double)value;
      return (double.IsNaN(num) || ((num >= 0.0) && !double.IsPositiveInfinity(num)));
    }

    protected bool HasItemWidth
    {
      get { return (!double.IsNaN(ItemWidth) && ItemWidth != 0); }
    }

    protected bool HasItemHeight
    {
      get { return (!double.IsNaN(ItemHeight) && ItemHeight != 0); }
    }

    protected override Size CustomMeasureOverride(Size constraint)
    {
      Lines = 0;

      UVSize lineSize = new UVSize(Orientation);
      UVSize totalSize = new UVSize(Orientation);
      UVSize constraintSize = new UVSize(Orientation, constraint.Width, constraint.Height);

      Size availableSize = new Size(HasItemWidth ? ItemWidth : constraint.Width, HasItemHeight ? ItemHeight : constraint.Height);
      int start = 0;
      int end = 0;
      bool exit;
      while (end < Children.Count)
      {
        UIElement currentElement = Children[end];
        if (currentElement != null)
        {
          MeasureElement(currentElement, availableSize);
          UVSize currentElementSize = new UVSize(Orientation, CalculateElementSize(currentElement));
          if ((lineSize.U + currentElementSize.U).GreaterThan(constraintSize.U))
          {
            exit = !MeasureLine(start, end, constraintSize.U - lineSize.U, ref lineSize);

            totalSize.U = Math.Max(lineSize.U, totalSize.U);
            totalSize.V += lineSize.V;

            if (exit)
              return totalSize.Size;
            
            lineSize = currentElementSize;
            if (lineSize.U.GreaterThan(constraintSize.U))
            {
              exit = !MeasureLine(start, end, constraintSize.U - lineSize.U, ref lineSize);

              totalSize.U = Math.Max(currentElementSize.U, totalSize.U);
              totalSize.V += currentElementSize.V;

              if (exit)
                return totalSize.Size;
              
              lineSize = new UVSize(Orientation);
            }
            start = end;
          }
          else
          {
            lineSize.U += currentElementSize.U;
            lineSize.V = Math.Max(currentElementSize.V, lineSize.V);
          }
        }
        end++;
      }

      MeasureLine(start, Children.Count, constraintSize.U - lineSize.U, ref lineSize);

      totalSize.U = Math.Max(lineSize.U, totalSize.U);
      totalSize.V += lineSize.V;

      return totalSize.Size;
    }
  }
}